# 网格搜索
"""
现在我们知道了如果评估一个模型的泛化能力，下面继续学习通过调参来提升模型的泛化
性能。第 2 章和第 3 章中讨论过 scikit-learn 中许多算法的参数设置，在尝试调参之前，
重要的是要理解参数的含义。找到一个模型的重要参数（提供最佳泛化性能的参数）的取
值是一项棘手的任务，但对于几乎所有模型和数据集来说都是必要的。由于这项任务如此
常见，所以 scikit-learn 中有一些标准方法可以帮你完成。最常用的方法就是网格搜索
（grid search），它主要是指尝试我们关心的参数的所有可能组合。

考虑一个具有 RBF（径向基函数）核的核 SVM 的例子，它在 SVC 类中实现。正如第 2 章
中所述，它有 2 个重要参数：核宽度 gamma 和正则化参数 C。假设我们希望尝试 C 的取值
为 0.001、0.01、0.1、1、10 和 100，gamma 也取这 6 个值。由于我想要尝试的 C 和 gamma
都有 6 个不同的取值，所以总共有 36 种参数组合。所有可能的组合组成了 SVM 的参数设
置表（网格）
"""
import mglearn.plots
import numpy as np
import pandas as pd
from IPython.core.display_functions import display
from matplotlib import pyplot as plt
from pandas.core.common import random_state
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split, cross_val_score, GridSearchCV, StratifiedKFold, ParameterGrid
from sklearn.svm import SVC

# 简单网格搜索
'''
我们可以实现一个简单的网格搜索，在 2 个参数上使用 for 循环，对每种参数组合分别训
练并评估一个分类器：
'''
iris = load_iris()
# 简单的网格搜索实现
X_train,X_test,y_train,y_test = train_test_split(iris.data,iris.target,random_state=0)
print("Size of training set: {} size of test set: {}".format(
 X_train.shape[0], X_test.shape[0]))

best_score = 0

for gamma in [0.001,0.01,1,10,100]:
    for C in [0.001,0.01,1,10,100]:
        svm = SVC(C=C, gamma=gamma)
        svm.fit(X_train,y_train)
        score = svm.score(X_test,y_test)
        if score > best_score:
            best_score = score
            best_parameters = {'C':C,'gamma':gamma}

print("Best score: {:.2f}".format(best_score))
print("Best parameters: {}".format(best_parameters))
# Best score: 0.97
# Best parameters: {'C': 100, 'gamma': 0.001}

# 参数过拟合的风险与验证集
'''
看到这个结果，我们可能忍不住要报告，我们找到了一个在数据集上精度达到 97% 的模
型。然而，这种说法可能过于乐观了（或者就是错的），其原因如下：我们尝试了许多不
同的参数，并选择了在测试集上精度最高的那个，但这个精度不一定能推广到新数据上。
由于我们使用测试数据进行调参，所以不能再用它来评估模型的好坏。我们最开始需要将
数据划分为训练集和测试集也是因为这个原因。我们需要一个独立的数据集来进行评估，
一个在创建模型时没有用到的数据集。

为了解决这个问题，一种方法是再次划分数据，这样我们得到 3 个数据集：用于构建模型
的训练集，用于选择模型参数的验证集（开发集），用于评估所选参数性能的测试集。

模型性能：验证集最高分数为96%，测试集分数为92%，低于之前的97%。这可能是因为训练数据减少。
数据集划分的重要性：训练集、验证集和测试集的区分至关重要，以避免测试集信息泄漏。测试集应仅用于最终评估，而训练集和验证集用于模型选择和探索性分析。
模型评估的注意事项：在测试集上评估多个模型并选择最佳模型会导致对模型精度的过度乐观估计。

'''

mglearn.plots.plot_threefold_split()

'''
利用验证集选定最佳参数之后，我们可以利用找到的参数设置重新构建一个模型，但是要同时在训练数据和验证数据上进行训练。
这样我们可以利用尽可能多的数据来构建模型。
其实现如下所示：
'''

X_trainval,X_test,y_trainval,y_test = train_test_split(iris.data,iris.target,random_state=0)
# 将训练+验证集划分为训练集与验证集
X_train,X_valid,y_train,y_valid = train_test_split(X_trainval,y_trainval,random_state=1)
print("Size of training set: {} size of validation set: {} size of test set:"
 " {}\n".format(X_train.shape[0], X_valid.shape[0], X_test.shape[0]))

best_score = 0

for gamma in [0.001, 0.01, 0.1, 1, 10, 100]:
 for C in [0.001, 0.01, 0.1, 1, 10, 100]:
     # 对每种参数组合都训练一个SVC
     svm = SVC(gamma=gamma, C=C)
     svm.fit(X_train, y_train)
     # 在验证集上评估SVC
     score = svm.score(X_valid, y_valid)
     # 如果我们得到了更高的分数，则保存该分数和对应的参数
     if score > best_score:
         best_score = score
         best_parameters = {'C': C, 'gamma': gamma}

# 在训练+验证集上重新构建一个模型，并在测试集上进行评估
svm = SVC(**best_parameters)
svm.fit(X_trainval, y_trainval)
test_score = svm.score(X_test, y_test)
print("Best score on validation set: {:.2f}".format(best_score))
print("Best parameters: ", best_parameters)
print("Test set score with best parameters: {:.2f}".format(test_score))

'''
验证集上的最高分数是 96%，这比之前略低，可能是因为我们使用了更少的数据来训练模
型（现在 X_train 更小，因为我们对数据集做了两次划分）。但测试集上的分数（这个分数
实际反映了模型的泛化能力）更低，为 92%。因此，我们只能声称对 92% 的新数据正确
分类，而不是我们之前认为的 97% ！
训练集、验证集和测试集之间的区别对于在实践中应用机器学习方法至关重要。任何根据
测试集精度所做的选择都会将测试集的信息“泄漏”（leak）到模型中。因此，保留一个单
独的测试集是很重要的，它仅用于最终评估。好的做法是利用训练集和验证集的组合完成
所有的探索性分析与模型选择，并保留测试集用于最终评估——即使对于探索性可视化也
是如此。严格来说，在测试集上对不止一个模型进行评估并选择更好的那个，将会导致对
模型精度过于乐观的估计。

总结：
模型性能：
    验证集最高分数为96%，较之前略有下降，原因可能是训练数据量减少（X_train变小，因数据集被两次划分）。
    测试集分数为92%，低于之前认为的97%，这反映了模型对新数据的泛化能力。
数据集划分的重要性：
    训练集、验证集和测试集的区分在机器学习实践中至关重要，以防止测试集信息泄漏到模型中。
    应保留一个独立的测试集，仅用于最终评估。
    建议使用训练集和验证集的组合进行所有探索性分析和模型选择，即使在探索性可视化中也应如此。
模型评估的注意事项：
    在测试集上评估多个模型并选择最佳模型，会导致对模型精度的过度乐观估计。
'''

# 带交叉验证的网格搜索
'''
虽然将数据划分为训练集、验证集和测试集的方法是可行且常用的，但这种方法对数据的划分方式相当敏感。
例如，网格搜索在不同情况下选择的最佳参数可能不同，如 'C': 10, 'gamma': 0.001 和 'C': 100, 'gamma': 0.001。
为了更好地估计模型的泛化性能，可以使用交叉验证来评估每种参数组合的性能，而不是仅将数据单次划分为训练集与验证集。
在 scikit-learn 中，可以使用 GridSearchCV 结合交叉验证来实现这一点。
这种方法通过多次划分数据并评估模型性能，能够更准确地反映模型在新数据上的表现。
'''
for gamma in [0.001, 0.01, 0.1, 1, 10, 100]:
 for C in [0.001, 0.01, 0.1, 1, 10, 100]:
     # 对于每种参数组合都训练一个SVC
     svm = SVC(gamma=gamma, C=C)
     # 执行交叉验证
     scores = cross_val_score(svm, X_trainval, y_trainval, cv=5)
     # 计算交叉验证平均精度
     score = np.mean(scores)
     # 如果我们得到了更高的分数，则保存该分数和对应的参数
     if score > best_score:
         best_score = score
         best_parameters = {'C': C, 'gamma': gamma}

# 在训练+验证集上重新构建一个模型
svm = SVC(**best_parameters)
svm.fit(X_trainval, y_trainval)

'''
要想使用 5 折交叉验证对 C 和 gamma 特定取值的 SVM 的精度进行评估，需要训练 36×5 = 180 个模型。
你可以想象，使用交叉验证的主要缺点就是训练所有这些模型所需花费的时间。
'''

# 下面的可视化说明了上述代码如何选择最佳参数设置：
mglearn.plots.plot_cross_val_selection()
# plt.show()

'''
对于每种参数设置（图中仅显示了一部分），需要计算 5 个精度值，交叉验证的每次划分
都要计算一个精度值。然后，对每种参数设置计算平均验证精度。最后，选择平均验证精
度最高的参数，用圆圈标记。
'''

'''
如前所述，交叉验证是在特定数据集上对给定算法进行评估的一种方法。但它通常与网格搜索等参数搜索方法结合使用。
因此，许多人使用交叉验证（cross-validation）这一术语来通俗地指代带交叉验证的网格搜索。
'''

# 划分数据、运行网格搜索并评估最终参数，这整个过程所如下所示。
mglearn.plots.plot_grid_search_overview()
# plt.show()

'''
由于带交叉验证的网格搜索是一种常用的调参方法，因此scikit-learn 提供了GridSearchCV 类，
它以估计器（estimator）的形式实现了这种方法。要使用 GridSearchCV类，你首先需要用一个字典指定要搜索的参数。
然后 GridSearchCV 会执行所有必要的模型拟合。字典的键是我们要调节的参数名称（在构建模型时给出，在这个例子中是 C 和gamma），
字典的值是我们想要尝试的参数设置。如果 C 和 gamma 想要尝试的取值为 0.001、0.01、0.1、1、10 和 100，可以将其转化为下面这个字典：
'''
param_grid = {'C':[0.001,0.01,0.1,1,10,100],'gamma':[0.001,0.01,0.1,1,10,100]}

'''
现在我们可以使用模型（SVC）、要搜索的参数网格（param_grid）与要使用的交叉验证策略（比如 5 折分层交叉验证）将 GridSearchCV 类实例化：
'''

grid_search = GridSearchCV(SVC(),param_grid,cv=5)

'''
GridSearchCV 将使用交叉验证来代替之前用过的划分训练集和验证集方法。但是，我们仍需要将数据划分为训练集和测试集，以避免参数过拟合：
'''

X_train,X_test,y_train,y_test = train_test_split(iris.data,iris.target,random_state=0)

'''
我们创建的 grid_search 对象的行为就像是一个分类器，我们可以对它调用标准的 fit、predict 和 score 方法。
但我们在调用 fit 时，它会对 param_grid 指定的每种参数组合都运行交叉验证：
'''
grid_search.fit(X_train, y_train)

'''
拟合 GridSearchCV 对象不仅会搜索最佳参数，还会利用得到最佳交叉验证性能的参数在整个训练数据集上自动拟合一个新模型。
因此，fit 完成的工作相当于本节开头代码的结果。GridSearchCV 类提供了一个非常方便的接口，
可以用 predict 和 score 方法来访问重新训练过的模型。为了评估找到的最佳参数的泛化能力，我们可以在测试集上调用 score。
'''

print("Test set score: {:.2f}".format(grid_search.score(X_test, y_test)))
# Test set score: 0.97

'''
通过使用交叉验证选择参数，我们找到了一个在测试集上精度为97%的模型。重要的是，我们没有使用测试集来选择参数。
GridSearchCV对象的best_params_属性保存了找到的最佳参数，
而best_score_属性保存了交叉验证的最佳精度，即对于这种参数设置，不同划分的平均精度。
'''
print("Best parameters: {}".format(grid_search.best_params_))
print("Best cross-validation score: {:.2f}".format(grid_search.best_score_))

# Best parameters: {'C': 10, 'gamma': 0.1}
# Best cross-validation score: 0.97

'''
best_score_：保存的是交叉验证的平均精度，是在训练集上进行交叉验证得到的。
score 方法：使用的是在整个训练集上训练的模型，评估的是模型在测试集上的泛化性能。
详细说明
    best_score_：
        定义：best_score_ 是 GridSearchCV 在交叉验证过程中找到的最佳参数组合对应的平均精度。
        计算方式：在训练集上进行交叉验证，将训练集分成多个子集，每次用其中一个子集作为验证集，其余作为训练集，计算每个参数组合的平均精度。
        用途：用于评估不同参数组合在训练集上的表现，帮助选择最佳参数组合。
    score 方法：
        定义：score 方法用于评估模型在测试集上的表现。
        计算方式：使用在整个训练集上训练的模型，对测试集进行预测，计算模型的精度。
        用途：用于评估模型在未见过的数据上的泛化能力。
'''

'''
能够访问实际找到的模型，这有时是很有帮助的，比如查看系数或特征重要性。你可以用
best_estimator_ 属性来访问最佳参数对应的模型，它是在整个训练集上训练得到的：
'''

print("Best estimator:\n{}".format(grid_search.best_estimator_))
# Best estimator:
# SVC(C=10, gamma=0.1)

'''
由于 grid_search 本身具有 predict 和 score 方法，所以不需要使用 best_estimator_ 来进
行预测或评估模型。
'''

# 1. 分析交叉验证的结果
'''
将交叉验证的结果可视化通常有助于理解模型泛化能力对所搜索参数的依赖关系。
由于运行网格搜索的计算成本相当高，所以通常最好从相对比较稀疏且较小的网格开始搜索。
然后我们可以检查交叉验证网格搜索的结果，可能也会扩展搜索范围。
网格搜索的结果可以保存在 cv_results_ 属性中，它是一个字典，其中保存了搜索的所有内容。
最好将其转换成 pandas 数据框后再查看。
'''

results = pd.DataFrame(grid_search.cv_results_)
display(results.head())

'''
results 中每一行对应一种特定的参数设置。对于每种参数设置，交叉验证所有划分的结
果都被记录下来，所有划分的平均值和标准差也被记录下来。由于我们搜索的是一个二维
参数网格（C 和 gamma），所以最适合用热图可视化。我们首先提取平均验证分数，
然后改变分数数组的形状，使其坐标轴分别对应于 C 和 gamma：
'''

scores = np.array(results.mean_test_score).reshape(6, 6)

# 创建一个图形和轴
fig, ax = plt.subplots()

# 使用 imshow 绘制热点图
cax = ax.imshow(scores, cmap='viridis', interpolation='nearest')
# 添加颜色条
fig.colorbar(cax)

# 设置 x 轴和 y 轴的标签
ax.set_xticks(np.arange(6))
ax.set_yticks(np.arange(6))
ax.set_xticklabels(param_grid['gamma'])
ax.set_yticklabels(param_grid['C'])

# 设置 x 轴和 y 轴的标签
ax.set_xlabel('Gamma')
ax.set_ylabel('C')

# 在每个格子上添加数值标签
for i in range(6):
    for j in range(6):
        text = ax.text(j, i, f'{scores[i, j]:.2f}',
                       ha='center', va='center', color='white')

plt.show()

'''
热图是一种非常直观的方式来展示交叉验证结果，其中每个点对应于一次交叉验证和一种特定的参数设置。
颜色表示交叉验证的精度：浅色表示高精度，深色表示低精度。通过热图，我们可以清楚地看到模型对参数设置的敏感性。
'''

# 2. 在非网格的空间中搜索
'''
在某些情况下，尝试所有参数的所有可能组合（正如 GridSearchCV 所做的那样）并不是一个好主意。
例如，SVC 有一个 kernel 参数，根据所选择的 kernel，其他参数也是与之相关的。
如果 kernel='linear'，那么模型是线性的，只会用到 C 参数。
如果 kernel='rbf'，则需要使用 C 和 gamma 两个参数（但用不到类似 degree 的其他参数）。
在这种情况下，搜索 C、gamma 和 kernel 所有可能的组合没有意义：
如果 kernel='linear'，那么 gamma 是用不到的，尝试 gamma 的不同取值将会浪费时间。
为了处理这种“条件”（conditional）参数，GridSearchCV 的 param_grid 可以是字典组成的列表（a list of dictionaries）。
列表中的每个字典可扩展为一个独立的网格。包含内核与参数的网格搜索可以如下所示：
'''

param_grid = [{'kernel': ['rbf'],
 'C': [0.001, 0.01, 0.1, 1, 10, 100],
 'gamma': [0.001, 0.01, 0.1, 1, 10, 100]},
 {'kernel': ['linear'],
 'C': [0.001, 0.01, 0.1, 1, 10, 100]}]

'''
在第一个网格中，kernel 参数始终等于 'rbf'（注意 kernel 是一个长度为 1 的列表），而
C 和 gamma 都是变化的。在第二个网格中，kernel 参数始终等于 'linear'，只有 C 是变化
的。下面我们来应用这个更加复杂的参数搜索：
'''

grid_search = GridSearchCV(SVC(),param_grid,cv=5)
grid_search.fit(X_train, y_train)
print("Best parameters: {}".format(grid_search.best_params_))
print("Best cross-validation score: {:.2f}".format(grid_search.best_score_))

# Best parameters: {'C': 10, 'gamma': 0.1, 'kernel': 'rbf'}
# Best cross-validation score: 0.97

# 我们再次查看 cv_results_。正如所料，如果 kernel 等于 'linear'，那么只有 C 是变化的
results = pd.DataFrame(grid_search.cv_results_)
# 我们给出的是转置后的表格，这样更适合页面显示：
display(results.T)

# 3. 使用不同的交叉验证策略进行网格搜索
'''

GridSearchCV 默认对分类问题使用分层 k 折交叉验证（StratifiedKFold），对回归问题使用 k 折交叉验证（KFold）。
然而，你可以通过 cv 参数传入任何交叉验证分离器，以满足特定需求。这在处理非常大的数据集或非常慢的模型时特别有用，因为可以减少计算成本。
使用 ShuffleSplit 或 StratifiedShuffleSplit如果你只想将数据单次划分为训练集和验证集，
可以使用 ShuffleSplit 或 StratifiedShuffleSplit，并设置 n_iter=1。
ShuffleSplit 适用于回归问题，而 StratifiedShuffleSplit 适用于分类问题，以保持类别比例。


（1）嵌套交叉验证（Nested Cross-Validation）
嵌套交叉验证是一种更稳健的模型评估方法，它通过外层循环和内层循环来评估模型的泛化能力。
外层循环负责将数据划分为训练集和测试集，
内层循环则在训练集上进行网格搜索以找到最佳参数。
这种方法可以减少因数据划分方式不同而导致的结果不稳定问题。

实现嵌套交叉验证
在 scikit-learn 中，可以使用 cross_val_score 函数来实现嵌套交叉验证。具体步骤如下：
定义内层交叉验证：用于网格搜索的交叉验证。
定义外层交叉验证：用于评估模型的泛化能力。
使用 cross_val_score：将 GridSearchCV 的实例作为模型传递给 cross_val_score
'''
scores = cross_val_score(GridSearchCV(SVC(),param_grid,cv=5),iris.data,iris.target,cv=5)
print("Cross-validation scores: ", scores)
print("Mean cross-validation score: ", scores.mean())
# Cross-validation scores:  [0.96666667 1.         0.9        0.96666667 1.        ]
# Mean cross-validation score:  0.9666666666666668

'''
这里我们在内层循环和外层循环中都使用了分层 5 折交叉验证。由于 param_grid 包含 36
种参数组合，所以需要构建 36×5×5 = 900 个模型，导致嵌套交叉验证过程的代价很高。
这里我们在内层循环和外层循环中使用相同的交叉验证分离器，但这不是必需的，你可以
在内层循环和外层循环中使用交叉验证策略的任意组合。理解上面单行代码的内容可能有
点困难，将其展开为 for 循环可能会有所帮助，正如我们在下面这个简化的实现中所做的那样：
'''


def nested_cv(X, y, inner_cv, outer_cv, Classifier, parameter_grid):
 outer_scores = []
 # 对于外层交叉验证的每次数据划分，split方法返回索引值
 for training_samples, test_samples in outer_cv.split(X, y):
     # 利用内层交叉验证找到最佳参数
     best_parms = {}
     best_score = -np.inf
     # 遍历参数
     for parameters in parameter_grid:
         # 在内层划分中累加分数
         cv_scores = []
         # 遍历内层交叉验证
         for inner_train, inner_test in inner_cv.split(
             X[training_samples], y[training_samples]):
             # 对于给定的参数和训练数据来构建分类器
             clf = Classifier(**parameters)
             clf.fit(X[inner_train], y[inner_train])
             # 在内层测试集上进行评估
             score = clf.score(X[inner_test], y[inner_test])
             cv_scores.append(score)
             # 计算内层交叉验证的平均分数
             mean_score = np.mean(cv_scores)
             if mean_score > best_score:
                 # 如果比前面的模型都要好，则保存其参数
                 best_score = mean_score
                 best_params = parameters
             # 利用外层训练集和最佳参数来构建模型
             clf = Classifier(**best_params)
             clf.fit(X[training_samples], y[training_samples])
             # 评估模型
             outer_scores.append(clf.score(X[test_samples], y[test_samples]))
 return np.array(outer_scores)

scores = nested_cv(iris.data, iris.target, StratifiedKFold(5),
 StratifiedKFold(5), SVC, ParameterGrid(param_grid))
print("Cross-validation scores: {}".format(scores))

'''
（2）交叉验证与网格搜索并行化
在使用 GridSearchCV 和 cross_val_score 时，可以通过设置 n_jobs 参数来利用多个 CPU 内核进行并行计算，从而显著减少计算时间。
n_jobs 参数可以设置为你想使用的 CPU 内核数量，设置为 -1 时将使用所有可用的内核。

注意事项
内存使用：使用多个内核可能会占用大量内存，特别是在处理大型数据集时。建议在并行构建大型模型时监视内存的使用情况。
嵌套并行化：scikit-learn 不允许并行操作的嵌套。例如，如果你在模型（如随机森林）中使用了 n_jobs 选项，那么就不能在 GridSearchCV 中使用它来搜索这个模型。
集群并行化：虽然 scikit-learn 目前不支持在集群内的多台机器上并行运行网格搜索和交叉验证，但可以使用 IPython 并行框架来进行并行网格搜索。
通过并行化，可以显著减少网格搜索和交叉验证的计算时间，从而提高模型评估的效率。
'''